/* Copyright (c) 2022-2022, LiWangQian<liwangqian@huawei.com> All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of
 *    conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list
 *    of conditions and the following disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used
 *    to endorse or promote products derived from this software without specific prior written
 *    permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
#pragma once

#include "memory.h"
#include "noncopyable.h"
#include "refcounted.h"
#include <functional>
#include <type_traits>

namespace libflexe::infra {

template <typename T, bool = is_refcounted_v<T>>
class intrusive_ptr : public noncopyable {
public:
    using value_type = T;
    using size_type = size_t;
    using pointer = std::add_pointer_t<T>;
    using const_pointer = std::add_const_t<pointer>;
    using reference = std::add_lvalue_reference_t<T>;
    using const_reference = std::add_const_t<reference>;

    ~intrusive_ptr()
    {
        release_ptr();
    }

    intrusive_ptr() = default;
    intrusive_ptr(pointer ptr) noexcept
        : ptr_{ptr}
    {}

    intrusive_ptr(intrusive_ptr &&other) noexcept
    {
        std::swap(this->ptr_, other.ptr_);
    }

    intrusive_ptr &operator=(intrusive_ptr &&other) noexcept
    {
        if (&other != this) {
            std::swap(this->ptr_, other.ptr_);
        }
        return *this;
    }

    template <typename X, typename Y>
    using Compatible = std::enable_if_t<std::is_convertible_v<X*, Y*>, void>;

    template <typename Y, typename = Compatible<Y, T>>
    intrusive_ptr(intrusive_ptr<Y> &&other) noexcept
    {
        swap(other);
    }

    template <typename Y, typename = Compatible<Y, T>>
    intrusive_ptr &operator=(intrusive_ptr<Y> &&other) noexcept
    {
        if (&other != this) {
            swap(other);
        }
        return *this;
    }

    template <typename Y, typename = Compatible<Y, T>>
    void swap(intrusive_ptr<Y> &other) noexcept
    {
        std::swap((T*)other.ptr_, this->ptr_);
    }

    pointer operator->() noexcept
    {
        return ptr_;
    }

    const_pointer operator->() const noexcept
    {
        return ptr_;
    }

    explicit operator bool() const noexcept
    {
        return ptr_ != nullptr;
    }

    bool unique() const noexcept
    {
        return true;
    }

    constexpr auto refcount() const noexcept
    {
        return 0u;
    }

    constexpr bool refcounted() const noexcept
    {
        return false;
    }

    template <typename X, typename Y>
    friend bool operator==(const intrusive_ptr<X> &x, const intrusive_ptr<Y> &y);

    template <typename X, typename Y>
    friend bool operator==(const intrusive_ptr<X> &x, std::nullptr_t);

private:
    void release_ptr()
    {
        infra::delete_object(ptr_);
        ptr_ = nullptr;
    }

    pointer ptr_{nullptr};
};

////////////////////////////////////////////////////////////////////////////////

template <typename T>
class intrusive_ptr<T, true> {
public:
    using value_type = T;
    using size_type = size_t;
    using pointer = std::add_pointer_t<T>;
    using const_pointer = std::add_const_t<pointer>;
    using reference = std::add_lvalue_reference_t<T>;
    using const_reference = std::add_const_t<reference>;

    ~intrusive_ptr()
    {
        release_ptr();
    }

    intrusive_ptr() = default;

    intrusive_ptr(pointer ptr, bool add_ref = true)
        : ptr_{ptr}
    {
        if (add_ref) ptr_->ref();
    }

    intrusive_ptr(const intrusive_ptr &other)
        : ptr_{other.ptr_}
    {
        ptr_->ref();
    }

    intrusive_ptr(intrusive_ptr &&other)
    {
        swap(other);
    }

    intrusive_ptr &operator=(const intrusive_ptr &other)
    {
        if (&other != this) {
            ptr_ = other.ptr_;
            ptr_->ref();
        }
        return *this;
    }

    intrusive_ptr &operator=(intrusive_ptr &&other)
    {
        if (&other != this) {
            swap(other);
        }
        return *this;
    }

    template <typename X, typename Y>
    using Compatible = std::enable_if_t<std::is_convertible_v<X*, Y*>, void>;

    template <typename Y, typename = Compatible<Y, T>>
    intrusive_ptr(const intrusive_ptr<Y> &other)
        : ptr_{other.ptr_}
    {
        ptr_->ref();
    }

    template <typename Y, typename = Compatible<Y, T>>
    intrusive_ptr(intrusive_ptr<Y> &&other)
    {
        swap(other);
    }

    template <typename Y, typename = Compatible<Y, T>>
    intrusive_ptr &operator=(const intrusive_ptr<Y> &other)
    {
        if (&other != this) {
            ptr_ = other.ptr_;
            ptr_->ref();
        }
        return *this;
    }

    template <typename Y, typename = Compatible<Y, T>>
    intrusive_ptr &operator=(intrusive_ptr<Y> &&other)
    {
        if (&other != this) {
            swap(other);
        }
        return *this;
    }

    template <typename Y, typename = Compatible<Y, T>>
    void swap(intrusive_ptr<Y> &other)
    {
        std::swap<T*>(other.ptr_, this->ptr_);
    }

    pointer operator->()
    {
        return ptr_;
    }

    const_pointer operator->() const
    {
        return ptr_;
    }

    explicit operator bool() const noexcept
    {
        return ptr_ != nullptr;
    }

    bool unique() const noexcept
    {
        return ptr_->count() == 1;
    }

    auto refcount() const noexcept
    {
        return ptr_->count();
    }

    constexpr bool refcounted() const noexcept
    {
        return true;
    }

    template <typename X, typename Y>
    friend bool operator==(const intrusive_ptr<X> &x, const intrusive_ptr<Y> &y);

    template <typename X>
    friend bool operator==(const intrusive_ptr<X> &x, std::nullptr_t);

private:
    void release_ptr()
    {
        if (ptr_ == nullptr) {
            return;
        }

        ptr_->unref();

        if (ptr_->count() == 0) {
            infra::delete_object(ptr_);
            ptr_ = nullptr;
        }
    }

    pointer ptr_{nullptr};
};

template <typename X, typename Y>
static inline bool operator==(const intrusive_ptr<X> &x, const intrusive_ptr<Y> &y)
{
    return x.ptr_ == y.ptr_;
}

template <typename X>
static inline bool operator==(const intrusive_ptr<X> &x, std::nullptr_t)
{
    return x.ptr_ == nullptr;
}

template <typename X, typename Y>
static inline bool operator!=(const intrusive_ptr<X> &x, const intrusive_ptr<Y> &y)
{
    return !(x == y);
}

template <typename X>
static inline bool operator!=(const intrusive_ptr<X> &x, std::nullptr_t)
{
    return !(x == nullptr);
}

template <typename X>
static inline bool operator!=(std::nullptr_t, const intrusive_ptr<X> &x)
{
    return !(x == nullptr);
}

template <typename T, typename ...Args>
static inline intrusive_ptr<T> make_intrusive_ptr(Args&& ...args)
{
    return intrusive_ptr<T>{infra::new_object<T>(std::forward<Args>(args)...)};
}

} // namespace libflexe::infra
